package per.cy.common.util;

import cn.hutool.core.io.IORuntimeException;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import per.cy.common.enums.CharsetTag;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import java.util.StringJoiner;
import java.util.stream.Stream;

/**
 * @author cy
 * date 2024-08-16
 */
public class FileUtils {
    private static final Log log = LogFactory.get();

    private static final int BUFFER_SIZE = 16384; // 16KB buffer size

    /**
     * 读取文件内容
     *
     * @param filePath 文件路径
     * @param charset  文件编码格式，例如"UTF-8", 默认为 UTF-8
     * @return 文件内容的字符串表示
     */
    public static String getFileContent(String filePath, String... charset) {
        // 检查文件路径是否为空或null
        if (filePath == null || filePath.isEmpty()) {
            throw new IllegalArgumentException("File path cannot be null or empty.");
        }

        String charSetLocal;

        // 检查编码格式是否为空或null
        if (charset == null || charset.length == 0) {
            charSetLocal = CharsetTag.CHARSET_UTF_8.getVal();
        } else {
            charSetLocal = charset[0];
        }

        // 检查字符集是否被支持
        checkCharset(charSetLocal);

        try {
            // 使用StringJoiner拼接文件内容，行之间使用系统特定的行分隔符分隔
            StringJoiner joiner = new StringJoiner(System.lineSeparator());

            // 使用Files工具类读取文件内容
            Path path = Paths.get(filePath);
            try (Stream<String> lines = Files.lines(path, Charset.forName(charSetLocal))) {
                lines.forEach(joiner::add);
            }

            // 返回读取到的文件内容
            return joiner.toString();
        } catch (IOException e) {
            // 如果发生IO异常，记录错误信息并抛出运行时异常
            log.error("Failed to read file: {} ", filePath, e);
            throw new RuntimeException("Failed to read file: " + filePath, e);
        }
    }

    /**
     * 将文件内容转换为Base64编码字符串
     *
     * @param filePath 文件路径
     * @return 返回文件内容的Base64编码字符串
     */
    public static String getFileContentToBase64Str(String filePath) {
        return Base64.getEncoder().encodeToString(getBytesFromFile(new File(filePath)));
    }


    /**
     * 将Base64编码的字符串解码后写入文件
     *
     * @param filePath  文件路径，包括文件名和扩展名
     * @param base64Str Base64编码的字符串
     */
    public static void writeFileContentFromBase64Str(String filePath, String base64Str) {
        // 解码Base64字符串为字节数组
        byte[] decodedBytes = Base64.getDecoder().decode(base64Str);

        try (FileOutputStream fos = new FileOutputStream(filePath)) {
            fos.write(decodedBytes);
        } catch (IOException e) {
            log.error("Failed to write file: {} ", filePath, e);
            throw new RuntimeException("Failed to write file: " + filePath);
        }

    }

    /**
     * 从输入流中读取文件内容
     * 如果文件大小超过整型最大值，将抛出异常(不能超过2GB)
     *
     * @param file 文件对象，包含文件路径和名称
     * @return 文件内容的字符串表示
     * @throws IORuntimeException 如果文件过大或读取文件失败
     */
    public static byte[] getBytesFromFile(File file) {

        // 获取文件大小
        long len = file.length();

        // 检查文件大小是否超过整型最大值
        if (len >= Integer.MAX_VALUE) {
            throw new IORuntimeException("File is larger then max array size");
        }

        // 根据文件大小创建字节数组，用于存储文件内容
        byte[] inputBytes = new byte[(int) len];

        // 定义实际读取的字节数
        int readLength;

        // 使用try-with-resources确保输入流的关闭
        try (InputStream is = Files.newInputStream(file.toPath())) {

            // 读取文件内容到字节数组
            readLength = is.read(inputBytes);

            // 检查是否完整读取了文件内容
            if (readLength < len) {
                throw new IOException("File length is [" + len + "] but read [" + readLength + "]!");
            }

            // 根据指定字符集转换字节数组为字符串并返回
            return inputBytes;
        } catch (IOException e) {
            // 记录错误日志
            log.error(e.getMessage(), e);
            // 抛出自定义的运行时异常
            throw new IORuntimeException("Failed to read file: " + file.getAbsolutePath(), e);
        }

    }


    /**
     * 将字符串内容写入指定文件
     * 如果文件不存在，将会创建一个新的文件；如果文件已经存在，则会覆盖原有文件内容
     *
     * @param filePath 文件路径
     * @param content  需要写入的字符串内容
     * @param charset  指定字符集，如"UTF-8"
     */
    public static void writeFileContent(String filePath, String content, String charset) {
        // 根据文件路径创建File对象
        File file = new File(filePath);

        // 检查文件是否存在，如果不存在则尝试创建
        if (!file.exists()) {
            boolean created = false;
            try {
                created = file.createNewFile();
            } catch (IOException e) {
                log.error("Failed to create the file: " + filePath, e);
                throw new RuntimeException("Failed to create the file: " + filePath);
            }
            if (!created) {
                throw new RuntimeException("Failed to create the file: " + filePath);
            }
        }

        // 根据输入的字符集名称获取Charset对象
        Charset cs;
        try {
            cs = Charset.forName(charset);
        } catch (IllegalArgumentException e) {
            // 如果字符集无效，抛出异常
            throw new IllegalArgumentException("Invalid charset: " + charset, e);
        }

        // 使用指定字符集创建OutputStreamWriter，用于写入文件内容
        try (OutputStreamWriter writer = new OutputStreamWriter(Files.newOutputStream(file.toPath()), cs)) {
            // 写入字符串内容到文件
            writer.write(content);
        } catch (IOException e) {
            // 如果发生IO异常，记录错误信息并抛出运行时异常
            log.error("Failed to write to file: {} ", filePath, e);
            throw new RuntimeException("Failed to write to file: " + filePath, e);
        }
    }


    /**
     * 使用RSA和AES混合加密机制加密文件
     * 首先使用AES算法加密文件，然后使用RSA公钥加密AES密钥
     *
     * @param inputPath        待加密文件的路径
     * @param outputPath       加密后文件的保存路径
     * @param charset          文件内容的字符集
     * @param rsaPublicKeyPath RSA公钥的路径
     * @return 加密后的AES密钥
     */
    public static String encryptFileByAES(String inputPath, String outputPath, String charset, String rsaPublicKeyPath) {
        // 获取RSA公钥
        PublicKey rsaPublicKey = RSAUtil.getPublicKey(rsaPublicKeyPath, charset);
        String aesKey = null;
        try {
            // 生成AES-256密钥并转换为字符串形式
            aesKey = AESUtil.keyToString();
            // 使用生成的AES密钥加密文件
            AESUtil.encrypt(inputPath, outputPath, AESUtil.stringToKey(aesKey));
        } catch (NoSuchAlgorithmException e) {
            log.error("Failed to generate AES key", e);
            throw new RuntimeException("Failed to generate AES key", e);
        } catch (InvalidKeyException e) {
            log.error("Failed to encrypt file", e);
            throw new RuntimeException("Failed to encrypt file", e);
        }
        // 使用RSA公钥加密AES密钥，并返回加密后的AES密钥
        return RSAUtil.encrypt(aesKey, rsaPublicKey, charset);
    }

    /**
     * 使用RSA和AES混合加密方式解密文件
     * 该方法首先使用RSA私钥解密AES密钥，然后使用解密后的AES密钥解密指定文件
     *
     * @param inputPath         待解密文件的路径
     * @param outputPath        解密后文件的保存路径
     * @param charset           字符编码，用于确保字符串正确处理
     * @param rsaPrivateKeyPath RSA私钥文件的路径，用于解密AES密钥
     * @param aesEncryptedKey   加密后的AES密钥字符串，用于解密文件
     */
    public static void decryptFileAES(String inputPath, String outputPath, String charset, String rsaPrivateKeyPath, String aesEncryptedKey) {
        // 获取RSA私钥，用于解密AES密钥
        PrivateKey privateKey = RSAUtil.getPrivateKey(rsaPrivateKeyPath, charset);

        // 使用RSA私钥解密加密后的AES密钥字符串
        String aesKey = RSAUtil.decrypt(aesEncryptedKey, privateKey, charset);

        try {
            // 使用解密后的AES密钥解密指定文件
            AESUtil.decrypt(inputPath, outputPath, AESUtil.stringToKey(aesKey));
        } catch (InvalidKeyException e) {
            log.error("Failed to decrypt file", e);
            throw new RuntimeException("Failed to decrypt file", e);
        }
    }


    /**
     * 检查字符集是否被支持
     *
     * @param charset 要检查的字符集名称
     * @throws IllegalArgumentException 如果字符集不受支持，则抛出此异常
     */
    public static void checkCharset(String charset) {
        // 验证字符集是否有效
        if (!Charset.isSupported(charset)) {
            log.error("Unsupported charset: {}", charset);
            throw new IllegalArgumentException("Unsupported charset: " + charset);
        }
    }
}
